#!/usr/bin/env nush
#
# @file nuke
# The Nu software construction tool.
#
# @copyright Copyright (c) 2007 Tim Burks, Radtastical Inc.
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

;; basic declarations that allow nuke to run in mininush
(set exit (NuBridgedFunction functionWithName:"exit" signature:"vi"))
(set NSUTF8StringEncoding 4)

;; system-level helpers
(function SH (command)
     (puts "nuke: #{command}")
     (set result (system command))
     (if result
         (puts "nuke: terminating on command error (return code #{result})")
         (exit result))
     result)

(function command-exists (cmd) 
     (not (system "command -v #{cmd} >/dev/null 2>&1")))

(macro ifDarwin (*body) `(if (eq (uname) "Darwin") ,@*body))
(macro ifLinux (*body) `(if (eq (uname) "Linux") ,@*body))
(macro ifFreeBSD (*body) `(if (eq (uname) "FreeBSD") ,@*body))
(macro ifOpenSolaris (*body) `(if (eq (uname) "OpenSolaris") ,@*body))
(macro ifGNUstep (*body) `(if (ne (uname) "Darwin") ,@*body))

(function isDarwin () (eq (uname) "Darwin"))
(function isLinux () (eq (uname) "Linux"))
(function isFreeBSD () (eq (uname) "FreeBSD"))
(function isOpenSolaris () (eq (uname) "OpenSolaris"))
(function isGNUstep () (not (eq (uname) "Darwin")))

;; Special check for Snow Leopard
;; Darwin kernel version for Snow Leopard is 10.x.x
(function isSnowLeopard ()
     (and (isDarwin)
          (== "10" ((NSString stringWithShellCommand:"uname -r | cut -f1 -d.") chomp))))

;; Special check for Lion
;; Darwin kernel version for Lion is 11.x.x
(function isLion ()
     (and (isDarwin)
          (== "11" ((NSString stringWithShellCommand:"uname -r | cut -f1 -d.") chomp))))

(class NSString
     ;; Change the extension at the end of a file name to a new specified value.
     (- (id) stringByReplacingPathExtensionWith:(id) newExtension is
        ((self stringByDeletingPathExtension) stringByAppendingPathExtension:newExtension))
     
     ;; Get the directory name component of a file name.
     (- (id) dirName is
        (set components (self componentsSeparatedByString:"/"))
        (set result ((NSMutableString alloc) init))
        ((- (components count) 1) times:
         (do (n)
             (result appendString:(components objectAtIndex:n))
             (result appendString:"/")))
        result)
     
     ;; Get the file name component of a file name.
     (- (id) fileName is
        (set components (self componentsSeparatedByString:"/"))
        (components objectAtIndex:(- (components count) 1))))

(class NSArray
     ;; Join the non-null members of an array into a string with array elements separated by spaces.
     (- (id) join is
        ((self select:(do (x) x)) componentsJoinedByString:" "))
     
     ;; Get the element of an array with the maximum value (as obtained using the specified block).
     (- (id) maximum:(id)block from:(id)initial is
        (self reduce:
              (do (max d)
                  (set value (block d))
                  (if (> value max) (then value) (else max)))
              from: initial)))

(class NuCell
     ;; Join the elements of a list into a string with list elements separated by spaces.
     (- (id) join is
        ((NSArray arrayWithList:self) join)))

;; creates a named directory if it does not already exist
(function make-directory (dir)
     (set components (dir componentsSeparatedByString:"/"))
     ((components count) times:
      (do (i)
          (set partial
               ((components subarrayWithRange:(list 0 (+ i 1))) componentsJoinedByString:"/"))
          (unless (eq (NSFileManager directoryExistsNamed:partial) YES)
                  (SH "mkdir '#{partial}'"))))
     0)

;; returns an array of filenames matching a given pattern
(function filelist (pattern)
     (set regex (NSRegularExpression regexWithPattern:pattern))
     (set results ((NSMutableSet alloc) init))
     (let (enumerator ((NSFileManager defaultManager) enumeratorAtPath:"."))
          (while (set filename (enumerator nextObject))
                 (if (regex findInString:(filename stringValue))
                     (results addObject:filename))))
     results)

;; we need support for optional arguments with default values...
(function filelistWithRoot (pattern root)
     (set regex (NuRegex regexWithPattern:pattern))
     (set results ((NSMutableSet alloc) init))
     (let (enumerator ((NSFileManager defaultManager) enumeratorAtPath:root))
          (while (set filename (enumerator nextObject))
                 (if (regex findInString:(filename stringValue))
                     (results addObject:filename))))
     results)

;; @abstract A task description, complete with action and dependency information.
;; @discussion NukeTasks are used to represent tasks in nuke,
;; the Nu build tool.  A NukeTask may be associated with a file
;; or may represent an abstract set of operations.  Tasks have
;; dependencies; each dependency must be satisfied before
;; the task action is attempted.  In nuke, tasks are created
;; using the <b>task</b> and <b>file</b> macros.
(class NukeTask is NSObject
     (ivar (id) name)			 ;; the name of the task
     (ivar (id) taskDescription) ;; the description of the task
     (ivar (id) dependencies)	 ;; an array of references to other tasks
     (ivar (id) action)			 ;; a block which performs the necessary build action
     (ivar (int) isFile)		 ;; if nonzero, task is a file task
     (ivar (id) result)			 ;; the result of executing the task
     
     ;; @discussion Create a task with a specified name.
     (- (id) initWithName:(id) name is
        (super init)
        (set @name name)
        (set @isFile 1)
        (set @action (do (target) (if $verbose (puts "null action for target #{(target name)}")) 0))
        (set @dependencies ((NSMutableArray alloc) init))
        self)
     
     ;; Get a task's name.
     (- (id) name is @name)
     
     ;; Get a task's description.
     (- (id) taskDescription is @taskDescription)
     
     ;; Get the array containing the task's dependencies.
     (- (id) dependencies is @dependencies)
     
     ;; Set the description of a task.
     (- (void) setTaskDescription:(id) taskDescription is (set @taskDescription taskDescription))
     
     ;; Set the action of a task to a specified block.
     (- (void) setAction:(id) action is (set @action action))
     
     ;; Indicate whether or not a task is a file creation task.
     (- (void) setIsFile:(int) f is (set @isFile f))
     
     ;; Determine whether or not a task is a file creation task.
     (- (int) isFile is @isFile)
     
     ;; Get a time stamp for the target of a task.
     (- (id) timestamp is
        (set date (NSFileManager creationTimeForFileNamed:@name))
        (cond ((eq @isFile 0) 0)
              (date           ((NSFileManager modificationTimeForFileNamed:@name) timeIntervalSinceReferenceDate))
              (else           0)))
     
     ;; Attempt to update a task, first by updating all its dependencies,
     ;; then, if no errors occurred, by performing the the action of a task.
     (- (id) update is
        (unless @result
                ;; first update all dependencies
                (set @result (@dependencies reduceLeft:
                                  (do (r d)
                                      (set intermediate (d update))
                                      (if $verbose (puts "task #{@name} intermediate dependency #{(d name)} result is #{intermediate} accumulating with #{r}"))
                                      (+ r intermediate))
                                  from:0))
                (if $verbose (puts "task #{@name} dependency result is #{@result}"))
                
                ;; continue only if there were no errors
                (if (eq @result 0)
                    ;; then get the largest timestamp -- it represents the newest dependency
                    (set dependency-timestamp
                         (@dependencies maximum:(do (d) (d timestamp)) from:0))
                    ;; if the largest timestamp is greater than or equal to this task's timestamp, perform the task's action
                    (if (or (eq (self timestamp) 0) (> dependency-timestamp (self timestamp)))
                        ;;(puts "task #{@name} dependency #{dependency-timestamp} self #{(self timestamp)}")
                        (if @action
                            (then (set @result (@action self))
                                  (unless @result (set @result 0)))
                            (else (set @result 0)))
                        nil))
                (if $verbose (puts "task #{@name} result is #{@result}")))
        @result))

;; do not use this directly. It is common code extracted from the file and task macros.
(macro task-helper (*body)
     `(progn
            (let ((__dependency nil)
                  (__action nil)
                  (__taskName ,(car *body))
                  (__taskDescription nil)
                  (__cursor nil))
                 
                 ;; if task contains a description, get it.
                 ;; otherwise, move on
                 (if (and (> ,((cdr *body) length) 0)
                          (eq ': ',(car (cdr *body))))
                     (then
                          (set __taskDescription ,(car (cdr (cdr *body))))
                          (set __cursor ',(cdr (cdr (cdr *body)))))
                     (else
                          (set __cursor ',(cdr *body))))
                 
                 ;; get dependencies
                 (if (and __cursor (eq (car __cursor) '=>))
                     (then
                          (set __cursor (cdr __cursor))
                          (while (and __cursor (!= (car __cursor) 'is))
                                 (set __dependency (eval (car __cursor)))
                                 (self addDependency:__taskName on:__dependency)
                                 (set __cursor (cdr __cursor))
                                 )))
                 
                 ;; get action
                 (if (and __cursor (eq (car __cursor) 'is))
                     (then
                          (set __action (cdr __cursor))
                          (set __block (eval (append '(do (target)) __action)))
                          (if __taskDescription
                              (then
                                   (self addTask:__taskName description:__taskDescription action:__block))
                              (else
                                   (self addTask:__taskName action:__block)))))
                 
                 __taskName)))


;; use this to define tasks that create files.
(macro file (*body)
     `(progn
            ((self taskNamed:(task-helper ,@*body)) setIsFile:1)))

;; use this to define tasks that DON'T create files.
(macro task (*body)
     `(progn
            ((self taskNamed:(task-helper ,@*body)) setIsFile:0)))

;; helper that finds momc, the datamodel compiler
(function momc-path ()
     (set momc nil)
     ('( "/Library/Application Support/Apple/Developer Tools/Plug-ins/XDCoreDataModel.xdplugin/Contents/Resources/momc"
         "/Developer/Library/Xcode/Plug-ins/XDCoreDataModel.xdplugin/Contents/Resources/momc"
         "/Developer/usr/bin/momc")
       each:
       (do (momc-path)
           (if (NSFileManager fileExistsNamed:momc-path)
               (set momc (momc-path replaceString:" " withString:-"\ ")))))
     (unless momc (NSException raise:@"NukeError" format:@"Can't find momc (data model compiler)."))
     momc)

(function select-compiler ()
     (ifDarwin
              ;; clang built-in to 10.7.3
              (set clang "/usr/bin/clang")
              (if (not (NSFileManager fileExistsNamed:clang))
                  ;; use xcode-select to get from e.g. /Developer or Xcode.app
                  (set DEVROOT (NSString stringWithShellCommand:"xcode-select -print-path"))
                  (set clang "#{DEVROOT}/usr/bin/clang")
                  (if (not (NSFileManager fileExistsNamed:clang))
                      (set clang nil))))
     (ifLinux (set clang "/usr/bin/clang"))
     ;; default to gcc
     (if clang clang (else "gcc")))

(function select-debugger ()
     (if (command-exists "lldb") "lldb" (else "gdb")))

;; use this to create all the compilation tasks for the files in the @c_files and @m_files collections
(macro compilation-tasks ()
     `(progn
            (unless @cc (set @cc (select-compiler)))
            (unless @cflags (set @cflags "-g"))
            (unless @mflags (set @mflags "-fobjc-exceptions"))
            (unless @includes (set @includes ""))
            (ifDarwin
                     (then (unless (and @arch (@arch length))
                                   (set @arch (list (NSString stringWithShellCommand:"uname -m")))))
                     (else (set @arch (list "x86-linux"))))
            (@arch each: (do (architecture) (system "mkdir -p build/#{architecture}")))
            
            (unless @ldflags ;; not for compilation, but common across all builds
                    (set @ldflags ((@frameworks map: (do (framework) " -framework #{framework}")) join)))
            
            ;; compile c files
            (set @c_objects (NSMutableDictionary dictionary))
            (@arch each: (do (architecture) (@c_objects setObject:(NSMutableArray array) forKey:architecture)))
            (@c_files each:
                 (do (sourceName)
                     (@arch each:
                            (do (architecture)
                                (set objectName "build/#{architecture}/")
                                (objectName appendString:((sourceName fileName) stringByReplacingPathExtensionWith:"o"))
                                ((@c_objects objectForKey:architecture) addObject: objectName)
                                (ifDarwin
                                         (then (set archflags "-arch #{architecture}"))
                                         (else (set archflags "")))
                                (file objectName => sourceName is
                                      (SH "#{@cc} #{@cflags} #{archflags} #{@includes} -c -o #{(target name)} #{sourceName}"))))))
            
            ;; compile objc files
            (set @m_objects (NSMutableDictionary dictionary))
            (@arch each: (do (architecture) (@m_objects setObject:(NSMutableArray array) forKey:architecture)))
            (@m_files each:
                 (do (sourceName)
                     (@arch each:
                            (do (architecture)
                                (set objectName "build/#{architecture}/")
                                (objectName appendString:((sourceName fileName) stringByReplacingPathExtensionWith:"o"))
                                ((@m_objects objectForKey:architecture) addObject: objectName)
                                (ifDarwin
                                         (then (set archflags "-arch #{architecture}"))
                                         (else (set archflags "")))
                                (file objectName => sourceName is
                                      (SH "#{@cc} #{@cflags} #{@mflags} #{archflags} #{@includes} -c -o #{(target name)} #{sourceName}"))))))
            
            ;(puts (@c_objects description))
            ;(puts (@m_objects description))
            
            ;; compile datamodels
            (@datamodels each:
                 (do (model)
                     (set modelName ((model componentsSeparatedByString:".") objectAtIndex:0))
                     (file "#{modelName}.mom" => "#{modelName}.xcdatamodel" is
                           (SH "#{(momc-path)} #{modelName}.xcdatamodel #{modelName}.mom"))))
            
            ;; cleanup
            (task "clean" is
                  (system "rm -rf build"))))

;; use this to create all the linking and assembly tasks to build a Cocoa application
(macro application-tasks ()
     `(progn
            ;; DEFAULT: if no application name is specified, use something dumb and complain.
            (unless @application
                    (set @application "Untitled")
                    (NSLog "Please name your application by setting @application in your Nukefile"))
            
            ;; DEFAULT: if no prefix is specified, use the path where nuke is installed.
            (unless @prefix
                    (set @prefix "#{((((NSProcessInfo processInfo) arguments) 0) dirName)}.."))
            
            ;; DEFAULT: if no application identifier is specifed, use something dumb and complain.
            (unless @application_identifier
                    (set @application_identifier "nu.programming.untitled")
                    (NSLog "Please set an @application_identifier in your Nukefile"))
            
            ;; DEFAULT: if no icon is specified, use the nu icon for the application.
            (unless @application_icon_file (set @application_icon_file "nu.icns"))
            (unless @icon_files (set @icon_files (array "#{@prefix}/share/nu/resources/nu.icns")))
            
            ;; DEFAULT: if no nib files are specified, use the standard (empty) Nu MainMenu.nib.
            ;; -- note -- we no longer do this, instead we just omit the NSMainNibFile entry from Info.plist
            ;;(unless @nib_files (set @nib_files (array "#{@prefix}/share/nu/resources/English.lproj/MainMenu.nib")))
            
            ;; DEFAULT: if no creator code is specified, use "????"
            (unless @application_creator_code (set @application_creator_code "????"))
            
            ;; app directory tasks
            (set @application_dir                    "#{@application}.app")
            (set @application_contents_dir           "#{@application_dir}/Contents")
            (set @application_executable_dir         "#{@application_contents_dir}/MacOS")
            (set @application_resource_dir           "#{@application_contents_dir}/Resources")
            (set @application_resource_localized_dir "#{@application_resource_dir}/English.lproj")
            
            ;; make the application directory structure
            ((list @application_dir
                   @application_contents_dir
                   @application_executable_dir
                   @application_resource_dir
                   @application_resource_localized_dir)
             each: (do (dir) (file dir is (make-directory (target name)))))
            
            ;; application executable
            (set @application_executable_name "#{@application_executable_dir}/#{@application}")
            (if (or (and @c_objects (@c_objects count) (((@c_objects allValues) objectAtIndex:0) count))
                    (and @m_objects (@m_objects count) (((@m_objects allValues) objectAtIndex:0) count)))
                (then
                     ;; application architecture-specific executable
                     (set @application_executables (NSMutableArray array))
                     (@arch each:
                            (do (architecture)
                                (set application_executable "build/#{architecture}/application-#{@application}")
                                (@application_executables addObject:application_executable)
                                (ifDarwin
                                         (then (set archflags "-arch #{architecture}"))
                                         (else (set archflags "")))
                                (file application_executable => (@c_objects objectForKey:architecture) (@m_objects objectForKey:architecture) is
                                      (set command "#{@cc} #{((@c_objects objectForKey:architecture) join)} #{((@m_objects objectForKey:architecture) join)} #{archflags} #{@cflags} #{@ldflags} -o '#{(target name)}'")
                                      (SH command))))
                     ;; application fat executable
                     (file @application_executable_name => @application_executable_dir @application_executables is
                           (ifDarwin
                                    (then (set command "lipo -create #{(@application_executables join)} -output '#{@application_executable_name}'"))
                                    (else (set command "cp '#{(@application_executables objectAtIndex:0)}' '#{@application_executable_name}'")))
                           (SH command)))
                (else
                     (file @application_executable_name => @application_executable_dir is
                           (SH "cp '#{@prefix}/bin/nush' '#{(target name)}'"))))
            
            ;; copy application_resources into the application
            (task "application_resources" => @application_resource_dir)
            ((list @nu_files @icon_files) each:
             (do (l)
                 (l each:
                    (do (f)
                        (set baseName ((f componentsSeparatedByString:"/") lastObject))
                        (set targetFile "#{@application_resource_dir}/#{baseName}")
                        (file targetFile => f @application_resource_dir is
                              (SH "cp '#{f}' '#{targetFile}'"))
                        (task "application_resources" => targetFile)))))
            ((list @nib_files @resources) each:
             (do (l)
                 (l each:
                    (do (f)
                        (set g ((f componentsSeparatedByString:"/") lastObject))
                        (file "#{@application_resource_localized_dir}/#{g}" => f @application_resource_localized_dir is
                              (SH "cp -R '#{f}' '#{((target name) dirName)}'")
                              0)
                        (task "application_resources" => "#{@application_resource_localized_dir}/#{g}")))))
            
            ; compile xib files into the application
            (@xib_files each:
                 (do (xib)
                     (set baseName ((xib componentsSeparatedByString:"/") lastObject))
                     (set targetFile (baseName stringByReplacingPathExtensionWith:"nib"))
                     (set nib "#{@application_resource_localized_dir}/#{targetFile}")
                     (file nib => xib @application_resource_localized_dir is
                           (SH "ibtool --errors --warnings --notices --output-format human-readable-text --compile #{nib} #{xib}"))
                     (task "application_resources" => nib)))
            
            ;; copy datamodels into the application
            (@datamodels each:
                 (do (model)
                     (set modelName ((model componentsSeparatedByString:".") objectAtIndex:0))
                     (task "application_#{modelName}" => "#{modelName}.mom" @application_resource_dir is
                           (SH "cp '#{modelName}.mom' '#{@application_resource_dir}/#{(modelName lastPathComponent)}.mom'"))
                     (task "application" => "application_#{modelName}")))
            
            ;; create the application_infoplist
            (set application_infoplist "#{@application_contents_dir}/Info.plist")
            (file application_infoplist => @application_contents_dir is
                  (set info (NSDictionary dictionaryWithList:
                                 (list "CFBundleDevelopmentRegion" "English"
                                       "CFBundleExecutable" @application
                                       "CFBundleIconFile" @application_icon_file
                                       "CFBundleIdentifier" @application_identifier
                                       "CFBundleInfoDictionaryVersion" "6.0"
                                       "CFBundleName" @application
                                       "CFBundlePackageType" "APPL"
                                       "CFBundleSignature" @application_creator_code
                                       "CFBundleVersion" "1.0"
                                       "NSHumanReadableCopyright" ""
                                       "NSPrincipalClass" "NSApplication")))
                  (if (or @nib_files @xib_files)
                      (info set:(NSMainNibFile:"MainMenu")))
                  (if @application_help_folder
                      (info setObject:@application_help_folder forKey:"CFBundleHelpBookFolder")
                      (info setObject:@application_help_folder forKey:"CFBundleHelpBookName"))
                  (if @info (info addEntriesFromDictionary: @info))
                  (info writeToFile:(target name) atomically:NO)
                  0)
            
            ;; write the application_pkginfo
            (unless @creator_code (set @creator_code "????"))
            (set application_pkginfo "#{@application_contents_dir}/PkgInfo")
            (file application_pkginfo => @application_contents_dir is
                  (SH "/bin/echo -n 'APPL#{@creator_code}' > '#{(target name)}'"))
            
            (task "application" => @application_executable_name "application_resources" application_infoplist application_pkginfo)
            
            (task "run" => "application" is
                  (SH "open '#{@application_dir}'"))
            
            (task "debug" => "application" is
                  (SH  "'#{@application_dir}/Contents/MacOS/#{@application}'"))
            
            (task "gdb" => "application" is
                  (SH  "#{(select-debugger)} '#{@application_dir}/Contents/MacOS/#{@application}'"))
            
            (task "clobber" => "clean" is
                  (system "rm -rf '#{@application_dir}'"))
            
            ; Build a disk image for distributing the application.
            (task "application_image" => "application" is
                  (system "rm -rf '#{@application}.dmg' dmg")
                  (system "mkdir dmg; cp -Rp '#{@application}.app' dmg")
                  (system "hdiutil create -srcdir dmg '#{@application}.dmg' -volname '#{@application}'")
                  (system "rm -rf dmg"))))


;; use this to create all the linking and assembly tasks to build a standalone "tool" executable
(macro tool-tasks ()
     `(
       (unless (and @arch (@arch length))
               (set @arch (list (NSString stringWithShellCommand:"uname -m"))))
       
       (unless @ldflags (set @ldflags ""))
       (unless @tool_extras (set @tool_extras ""))
       
       ;; executable architecture-specific executable
       (set @tool_executables (NSMutableArray array))
       (@arch each:
              (do (architecture)
                  (set tool_executable "build/#{architecture}/#{@tool}")
                  (@tool_executables addObject:tool_executable)
                  (file tool_executable => (@c_objects objectForKey:architecture) (@m_objects objectForKey:architecture) is
                        (set command "#{@cc} -arch #{architecture} #{((@c_objects objectForKey:architecture) join)} #{((@m_objects objectForKey:architecture) join)} #{@tool_extras} #{@ldflags} -o '#{(target name)}'")
                        (SH command))))
       
       ;; tool fat archive
       (set @tool_executable_name "#{@tool}")
       (file @tool_executable_name => @tool_executables is
             (if (> (@tool_executables count) 1)
                 (then (set command "lipo -create #{(@tool_executables join)} -output '#{@tool_executable_name}'"))
                 (else (set command "cp '#{(@tool_executables objectAtIndex:0)}' '#{@tool_executable_name}'")))
             (SH command))
       
       (task "tool" => @tool_executable_name)
       
       (task "run" => "tool" is
             (SH @tool))
       
       (task "gdb" => "tool" is
             (SH  "#{(select-debugger)} '#{@tool}'"))
       
       (task "clobber" => "clean" is
             (system "rm -rf '#{@tool}'"))))


;; use this to create all the linking and assembly tasks to build a Cocoa framework
(macro framework-tasks ()
     `(progn
            (unless (and @arch (@arch length))
                    (set @arch (list (NSString stringWithShellCommand:"uname -m"))))
            
            (ifDarwin
                     (then (set FRAMEWORK_ROOT "/Library/Frameworks"))
                     (else (set FRAMEWORK_ROOT "/usr/local/frameworks")))
            
            ;; framework directory tasks
            (set @framework_dir                    "#{@framework}.framework")
            (set @framework_versions_dir           "#{@framework_dir}/Versions")
            (set @framework_contents_dir           "#{@framework_versions_dir}/A")
            (set @framework_headers_dir            "#{@framework_contents_dir}/Headers")
            (set @framework_resource_dir           "#{@framework_contents_dir}/Resources")
            (set @framework_resource_localized_dir "#{@framework_resource_dir}/English.lproj")
            
            ((list @framework_dir
                   @framework_versions_dir
                   @framework_contents_dir
                   @framework_headers_dir
                   @framework_resource_dir
                   @framework_resource_localized_dir)
             each: (do (dir) (file dir is (make-directory (target name)))))
            
            (set @initflags (if @framework_initializer
                                (then "-Wl,-init -Wl,_#{@framework_initializer}")
                                (else "")))
            
            (set @framework_executable_name "#{@framework_contents_dir}/#{@framework}")
            
            ;; framework architecture-specific executable
            (set @framework_executables (NSMutableArray array))
            (@arch each:
                   (do (architecture)
                       (set framework_executable "build/#{architecture}/framework-#{@framework}")
                       (@framework_executables addObject:framework_executable)
                       (ifDarwin
                                (then (set archflags "-arch #{architecture}"))
                                (else (set archflags "")))
                       (ifDarwin
                                (then
                                     (if @framework_install_path
                                         (then (set framework_install_name "#{@framework_install_path}/#{@framework_executable_name}"))
                                         (else (set framework_install_name @framework_executable_name)))
                                     (set installnameflag "-install_name #{framework_install_name}"))
                                (else (set installnameflag "")))
                       (ifDarwin
                                (then (set dylibflag "-dynamiclib"))
                                (else (set dylibflag "-shared")))
                       (file framework_executable => (@c_objects objectForKey:architecture) (@m_objects objectForKey:architecture) is
                             (set command "#{@cc} #{((@c_objects objectForKey:architecture) join)} #{((@m_objects objectForKey:architecture) join)} #{archflags} #{@cflags} #{@ldflags} #{@initflags} #{installnameflag} #{dylibflag} -o '#{(target name)}'")
                             (SH command))))
            
            ;; framework fat executable
            (file @framework_executable_name => @framework_contents_dir @framework_executables is
                  (ifDarwin
                           (then (set command "lipo -create #{(@framework_executables join)} -output '#{@framework_executable_name}'"))
                           (else (set command "cp '#{(@framework_executables objectAtIndex:0)}' '#{@framework_executable_name}'")))
                  (SH command))
            
            ;; framework_resources
            (task "framework_resources" => @framework_resource_dir @framework_resource_localized_dir)
            ((list @nu_files @resource_files) each:
             (do (l)
                 (l each:
                    (do (f)
                        (set baseName ((f componentsSeparatedByString:"/") lastObject))
                        (set targetFile "#{@framework_resource_dir}/#{baseName}")
                        (file targetFile => f @framework_resource_dir is
                              (SH "cp '#{f}' '#{targetFile}'"))
                        (task "framework_resources" => targetFile)))))
            (@nib_files each:
                 (do (f)
                     (set g ((f componentsSeparatedByString:"/") lastObject))
                     (file "#{@framework_resource_localized_dir}/#{g}" => f @framework_resource_localized_dir is
                           (SH "cp -R '#{f}' '#{((target name) dirName)}'")
                           0)
                     (task "framework_resources" => "#{@framework_resource_localized_dir}/#{g}")))
            (@xib_files each:
                 (do (xib)
                     (set baseName ((xib componentsSeparatedByString:"/") lastObject))
                     (set targetFile (baseName stringByReplacingPathExtensionWith:"nib"))
                     (set nib "#{@framework_resource_localized_dir}/#{targetFile}")
                     (file nib => xib @framework_resource_localized_dir is
                           (SH "ibtool --errors --warnings --notices --output-format human-readable-text --compile #{nib} #{xib}"))
                     (task "framework_resources" => nib)))
            
            ;; copy filelist defined in @public_headers into the framework
            (task "copy_framework_headers" => @framework_headers_dir)
            (@public_headers each:
                 (do (h)
                     (set baseName (h lastPathComponent))
                     (set targetFile (@framework_headers_dir stringByAppendingPathComponent:baseName))
                     (set sourceFile h)
                     (file targetFile => h is
                           (SH "cp -p '#{sourceFile}' '#{targetFile}'"))
                     (task "copy_framework_headers" => targetFile @framework_headers_dir)))
            (task "framework" => "copy_framework_headers")
            
            ;; copy datamodels into the framework
            (@datamodels each:
                 (do (model)
                     (set modelName ((model componentsSeparatedByString:".") objectAtIndex:0))
                     (task "framework_#{modelName}" => "#{modelName}.mom" @framework_resource_dir is
                           (SH "cp '#{modelName}.mom' '#{@framework_resource_dir}/#{(modelName lastPathComponent)}.mom'"))
                     (task "framework" => "framework_#{modelName}")))
            
            ;; framework_infoplist
            (ifDarwin
                     (then (set framework_infoplist "#{@framework_resource_dir}/Info.plist")
                           (file framework_infoplist => @framework_resource_dir is
                                 (set info (NSDictionary dictionaryWithList:
                                                (list "CFBundleDevelopmentRegion" "English"
                                                      "CFBundleExecutable" @framework
                                                      "CFBundleIdentifier" @framework_identifier
                                                      "CFBundleGetInfoString" ""
                                                      "CFBundleInfoDictionaryVersion" "6.0"
                                                      "CFBundleName" @framework
                                                      "CFBundlePackageType" "FMWK"
                                                      "CFBundleSignature" @framework_creator_code
                                                      "CFBundleVersion" "0.1"
                                                      "NSHumanReadableCopyright" ""
                                                      )))
                                 (if @info (info addEntriesFromDictionary: @info))
                                 (info writeToFile:(target name) atomically:NO)
                                 0)
                           0)
                     (else (set framework_infoplist "#{@framework_resource_dir}/Info-gnustep.plist")
                           (file framework_infoplist => @framework_resource_dir is
                                 (set info <<-END
{
  NOTE = "Automatically generated, do not edit!";
  NSExecutable = "#{@framework}";
  NSMainNibFile = "";
  CFBundleIdentifier = "#{@framework_identifier}";
  NSPrincipalClass = "#{@framework}";
}
END)
                                 (info writeToFile:framework_infoplist atomically:NO)
                                 0)))
            
            ;; framework_links
            (task "framework_links" => @framework_versions_dir @framework_dir is
                  (unless (eq 1 (NSFileManager directoryExistsNamed: "#{@framework_versions_dir}/Current"))
                          (SH "cd #{@framework_versions_dir}; ln -sf A Current"))
                  (unless (eq 1 (NSFileManager directoryExistsNamed: "#{@framework_dir}/Headers"))
                          (SH "cd #{@framework_dir}; ln -sf Versions/Current/Headers Headers"))
                  (unless (eq 1 (NSFileManager directoryExistsNamed: "#{@framework_dir}/Resources"))
                          (SH "cd #{@framework_dir}; ln -sf Versions/Current/Resources Resources"))
                  (unless (eq 1 (NSFileManager fileExistsNamed: "#{@framework_dir}/Versions/Current/#{@framework}"))
                          (SH "cd #{@framework_dir}; ln -sf Versions/Current/#{@framework} #{@framework}"))
                  0)
            
            (task "framework" => @framework_executable_name @framework_headers_dir "framework_resources" "framework_links" framework_infoplist)
            
            (task "install" => "framework" is
                  (if @framework_extra_install
                      (@framework_extra_install))
                  (SH "sudo mkdir -p #{FRAMEWORK_ROOT}")
                  (SH "sudo rm -rf #{FRAMEWORK_ROOT}/#{@framework}.framework")
                  (SH "sudo cp -Rp #{@framework}.framework #{FRAMEWORK_ROOT}/#{@framework}.framework"))
            
            (task "test" => "framework" is
                  (SH "nutest test/test_*.nu"))
            
            (task "clobber" => "clean" is
                  (system "rm -rf '#{@framework_dir}'"))))


;; use this to create all the linking and assembly tasks to build a Cocoa bundle
(macro bundle-tasks ()
     `(progn
            (unless (and @arch (@arch length))
                    (set @arch (list (NSString stringWithShellCommand:"uname -m"))))
            
            ;; bundle directory tasks
            (set @bundle_dir                    "#{@bundle}.bundle")
            (set @bundle_contents_dir           "#{@bundle_dir}/Contents")
            (set @bundle_executable_dir         "#{@bundle_contents_dir}/MacOS")
            (set @bundle_resource_dir           "#{@bundle_contents_dir}/Resources")
            ((list @bundle_dir
                   @bundle_contents_dir
                   @bundle_executable_dir
                   @bundle_resource_dir)
             each: (do (dir) (file dir is (make-directory (target name)))))
            
            ;; bundle architecture-specific executable
            (set @bundle_executables (NSMutableArray array))
            (@arch each:
                   (do (architecture)
                       (set bundle_executable "build/#{architecture}/bundle-#{@bundle}")
                       (@bundle_executables addObject:bundle_executable)
                       (ifDarwin
                                (then (set archflags "-arch #{architecture}"))
                                (else (set archflags "")))
                       (file bundle_executable => (@c_objects objectForKey:architecture) (@m_objects objectForKey:architecture) is
                             (set command "#{@cc} #{((@c_objects objectForKey:architecture) join)} #{((@m_objects objectForKey:architecture) join)} #{archflags} #{@cflags} #{@ldflags} -bundle -o '#{(target name)}'")
                             (SH command))))
            
            ;; bundle fat executable
            (set @bundle_executable_name "#{@bundle_executable_dir}/#{@bundle}")
            (file @bundle_executable_name => @bundle_executable_dir @bundle_executables is
                  (set command "lipo -create #{(@bundle_executables join)} -output '#{@bundle_executable_name}'")
                  (SH command))
            
            ;; bundle_resources
            (task "bundle_resources" => @bundle_resource_dir @bundle_resource_localized_dir)
            ((list @nu_files @resource_files) each:
             (do (l)
                 (l each:
                    (do (f)
                        (set baseName ((f componentsSeparatedByString:"/") lastObject))
                        (set targetFile "#{@bundle_resource_dir}/#{baseName}")
                        (file targetFile => f @bundle_resource_dir is
                              (SH "cp '#{f}' '#{targetFile}'"))
                        (task "bundle_resources" => targetFile)))))
            
            ;; bundle_infoplist
            (set bundle_infoplist "#{@bundle_contents_dir}/Info.plist")
            (file bundle_infoplist => @bundle_contents_dir is
                  (set bundle_info (NSDictionary dictionaryWithList:
                                        (list "CFBundleDevelopmentRegion" "English"
                                              "CFBundleExecutable" @bundle
                                              "CFBundleIdentifier" @bundle_identifier
                                              "CFBundleInfoDictionaryVersion" "6.0"
                                              "CFBundleName" @bundle
                                              "CFBundlePackageType" "BNDL"
                                              "CFBundleSignature" @bundle_creator_code
                                              "CFBundleVersion" "0.1")))
                  (if @bundle_info (bundle_info addEntriesFromDictionary: @info))
                  (bundle_info writeToFile:(target name) atomically:NO)
                  0)
            
            ;; bundle_pkginfo
            (unless @bundle_creator_code (set @bundle_creator_code "????"))
            (set bundle_pkginfo "#{@bundle_contents_dir}/PkgInfo")
            (file bundle_pkginfo => @bundle_contents_dir is
                  (SH "/bin/echo -n 'APPL#{@creator_code}' > '#{(target name)}'"))
            
            (task "bundle" => @bundle_executable_name "bundle_resources" bundle_infoplist bundle_pkginfo)
            
            (task "clobber" => "clean" is
                  (system "rm -rf '#{@bundle_dir}'"))))


;; use this to create all the linking and assembly tasks to build a statically-linkable library
(macro library-tasks ()
     `(progn
            (unless (and @arch (@arch length))
                    (set @arch (list (NSString stringWithShellCommand:"uname -m"))))
            
            (unless @library_extras
                    (set @library_extras ""))
            
            ;; library architecture-specific executable
            (set @library_executables (NSMutableArray array))
            (@arch each:
                   (do (architecture)
                       (set library_executable "build/#{architecture}/#{@library}.a")
                       (@library_executables addObject:library_executable)
                       (file library_executable => (@c_objects objectForKey:architecture) (@m_objects objectForKey:architecture) is
                             (set command "libtool -static #{((@c_objects objectForKey:architecture) join)} #{((@m_objects objectForKey:architecture) join)} #{@library_extras} -o '#{(target name)}'")
                             (SH command))))
            
            ;; library fat archive
            (set @library_executable_name "#{@library}.a")
            (file @library_executable_name => @library_executables is
                  (if (> (@library_executables count) 1)
                      (then (set command "lipo -create #{(@library_executables join)} -output '#{@library_executable_name}'"))
                      (else (set command "cp '#{(@library_executables objectAtIndex:0)}' '#{@library_executable_name}'")))
                  (SH command))
            
            (task "library" => @library_executable_name)
            
            (task "clobber" => "clean" is
                  (system "rm -rf '#{@library_executable_name}'"))))


;; use this to create all the linking and assembly tasks to build a dynamically-linkable library
(macro dylib-tasks ()
     `(progn
            (unless (and @arch (@arch length))
                    (set @arch (list (NSString stringWithShellCommand:"uname -m"))))
            (set libext (ifDarwin (then "dylib") (else "so")))
            
            ;; library architecture-specific executable
            (set @library_executables (NSMutableArray array))
            (@arch each:
                   (do (architecture)
                       (set library_executable "build/#{architecture}/#{@dylib}.#{libext}")
                       (@library_executables addObject:library_executable)
                       (ifDarwin
                                (then (set dylibflag "-dynamiclib"))
                                (else (set dylibflag "-shared")))
                       (ifDarwin
                                (then (set archflags "-arch #{architecture}"))
                                (else (set archflags "")))
                       (file library_executable => (@c_objects objectForKey:architecture) (@m_objects objectForKey:architecture) is
                             (set command "#{@cc} #{((@c_objects objectForKey:architecture) join)} #{((@m_objects objectForKey:architecture) join)} #{archflags} #{@cflags} #{@ldflags} #{dylibflag} -o '#{(target name)}'")
                             (SH command))))
            
            ;; fat dynamic library
            (set @library_executable_name "#{@dylib}.#{libext}")
            (file @library_executable_name => @library_executables is
                  (ifDarwin
                           (then (set command "lipo -create #{(@library_executables join)} -output '#{@library_executable_name}'"))
                           (else (set command "cp '#{(@library_executables objectAtIndex:0)}' '#{@library_executable_name}'")))
                  (SH command))
            
            (task "dylib" => @library_executable_name)
            
            (task "clobber" => "clean" is
                  (system "rm -rf '#{@library_executable_name}'"))))


;; @abstract A project consisting of an interrelated set of NukeTasks.
;; @discussion NukeProjects gather together a related set of NukeTask task descriptions
;; and allow them to be more easily referred to by name.  There is typically
;; one NukeProject for a given run of nuke.  A Nukefile is evaluated inside
;; an instance method of a NukeProject; so all instance variables in a Nukefile
;; belong to the NukeProject instance.
(class NukeProject is NSObject
     
     ;; Get the dictionary of tasks managed by a project.
     (- (id) tasks is @tasks)
     
     ;; Get a task by its name.
     (- (id) taskNamed:(id) taskName is
        (set mytask (@tasks objectForKey:taskName))
        (if (eq mytask nil)
            (set mytask ((NukeTask alloc) initWithName:taskName))
            (@tasks setObject:mytask forKey:taskName))
        mytask)
     
     ;; Add a new task by to a project.
     ;; The new task is specified by its name.
     ;; The task action should be specified as a block.
     (- (id) addTask:(id) taskName action:(id) action is
        (set mytask (self taskNamed:taskName))
        (mytask setAction:action)
        mytask)
     
     ;; Add a new task by to a project.
     ;; The new task is specified by its name.
     ;; The task description is a string describing what it does.
     ;; The task action should be specified as a block.
     (- (id) addTask:(id) taskName description:(id) description action:(id) action is
        (set mytask (self taskNamed:taskName))
        (mytask setTaskDescription:description)
        (mytask setAction:action)
        mytask)
     
     ;; Add a dependency between named tasks.
     (- (void) addDependency:(id) taskName on:(id) dependency is
        (if (eq YES (dependency isKindOfClass:NSString))
            (then
                 (((self taskNamed:taskName) dependencies) addObject: (self taskNamed:dependency)))
            (else ;; if it's not a string, the dependency must be enumerable
                  (dependency each:
                       (do (element)
                           (self addDependency:taskName on:element))))))
     
     ;; Perform the tasks needed to complete a named target task.
     (- (void) nuke:(id) targetName is
        (set target (@tasks objectForKey:targetName))
        (if target
            (then (target update))
            (else (puts "error, unknown target: #{targetName}"))))
     
     ;; Initialize a NukeProject.
     (- (id) init is
        (super init)
        (set @tasks ((NSMutableDictionary alloc) init))
        self)
     
     ;; Load and evaluate code from a named file. This is typically used to read a Nukefile.
     (- (id) load: (id) filename is
        (ifGNUstep
                  (eval (parse (NSString stringWithContentsOfFile:filename)))
                  else
                  (eval (parse (NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:(set error (NuReference new))))))
        self))

;; Helper functions & extensions
(function includedInOutput (task)
     (and (!= "default" (task name)) (not (task isFile))))

(class NSString
     ;; Create a string consisting of the specified number of spaces.
     (+ (id) spaces: (id) count is
        (unless $spaces (set $spaces (NSMutableDictionary dictionary)))
        (unless (set spaces ($spaces objectForKey:count))
                (set spaces "")
                (set c count)
                (unless c (set c 0))
                (while (> c 0)
                       (spaces appendString:" ")
                       (set c (- c 1)))
                ($spaces setObject:spaces forKey:count))
        (NSMutableString stringWithString:spaces))
     
     ;; Test to see whether a string begins with a specified substring.
     (- (id) beginsWithString:(id) string is
        (set range (self rangeOfString:string))
        (and range (eq (range first) 0))))

;;;;;;;;;;;;;;;;;;;;;;;;;
;; main program
;;;;;;;;;;;;;;;;;;;;;;;;;

;; there's lots more that we could do here, like
;; adding command line options to get task lists
;; or to control the verbosity.

(set Nukefile "Nukefile")
(set target "default")

(set argv ((NuApplication sharedApplication) arguments))
(for ((set i 0) (< i (argv count)) (set i (+ i 1)))
     (case (set argi (argv i))
           ("--dependencies" (set $printDependencies YES))
           ("-d" (set $printDependencies YES))
           ("--describe" (set $printTaskDescriptions YES))
           ("-D" (set $printTaskDescriptions YES))
           ("--help" (set $printHelp YES))
           ("-h" (set $printHelp YES))
           ("--nukefile" (set Nukefile (argv (set i (+ i 1)))))
           ("-f" (set Nukefile (argv (set i (+ i 1)))))
           ("--tasks" (set $printTasks YES))
           ("-T" (set $printTasks YES))
           ("--verbose" (set $verbose YES))
           ("-v" (set $verbose YES))
           ("--version" (system "nush -v") (exit 0))
           ("-V" (system "nush -v") (exit 0))
           (else
                (if ((argi beginsWithString:"-"))
                    (puts "nuke: invalid option: #{argi}")
                    (exit -1)
                    (else
                         (set target argi))))))

(if $printHelp
    (puts "")
    (puts "Usage: nuke [-f nukefile] {options} target")
    (puts "")
    (puts "Options include:")
    (puts "")
    (puts "  --dependencies   (-d)")
    (puts "      Display all tasks with their dependencies, then exit")
    (puts "  --describe       (-D)")
    (puts "      Display all tasks with their descriptions, then exit")
    (puts "  --help           (-h)")
    (puts "      Display program help")
    (puts "  --nukefile FILE  (-f)")
    (puts "      use FILE as the nukefile")
    (puts "  --tasks          (-T)")
    (puts "      Display list of available tasks, then exit")
    (puts "  --verbose        (-v)")
    (puts "      Log message to standard output")
    (puts "  --version        (-V)")
    (puts "      Display the program version")
    (puts "")
    (exit 0))

(unless (or $printTasks (or $printTaskDescriptions $printDependencies))
        (then (puts "Using #{Nukefile} with target #{target}."))
        (else (puts "Using #{Nukefile}.")))

;; We expect there to be a Nukefile in the current directory
;; or in one of its containing directories
(function climbToFile (filename)
     (set path (((NSProcessInfo processInfo) environment) objectForKey:"PWD"))
     (until (or (eq path "/")
                ((NSFileManager defaultManager) fileExistsAtPath:(+ path "/" filename)))
            (set path (path stringByDeletingLastPathComponent)))
     (if ((NSFileManager defaultManager) fileExistsAtPath:(+ path "/" filename))
         (puts "nuke: running in #{path}")
         ((NSFileManager defaultManager) changeCurrentDirectoryPath:path)))

;; bridge a standard C functionWithName
(set exit (NuBridgedFunction functionWithName:"exit" signature:"vi"))

(try
    (climbToFile Nukefile)
    (set project (((NukeProject alloc) init) load:Nukefile))
    (catch (exception)
           (puts "nuke error: #{(exception reason)}")
           (exit -1)))

(if $printTasks
    (set longestTaskNameLength 0)
    (set maxDescriptionLength 40)
    (puts "")
    ((((project tasks) allKeys) sort) each:
     (do (key)
         (set task ((project tasks) objectForKey:key))
         (if (includedInOutput (task))
             (if (> (key length) longestTaskNameLength)
                 (set longestTaskNameLength (key length))))))
    
    ((((project tasks) allKeys) sort) each:
     (do (key)
         (set task ((project tasks) objectForKey:key))
         (if (includedInOutput (task))
             (set spacesToDescription (NSString spaces:(+ (- longestTaskNameLength ((task name) length)) 2)))
             (if (task taskDescription)
                 (then
                      (if (> ((task taskDescription) length) maxDescriptionLength)
                          (then
                               (set taskDescription "#{((task taskDescription) substringToIndex:maxDescriptionLength)}..."))
                          (else
                               (set taskDescription (task taskDescription))))
                      (puts "nuke #{key}#{spacesToDescription}# #{taskDescription}"))
                 (else
                      (puts "nuke #{key}#{spacesToDescription}#")))
             (if $verbose (puts (send (task action) stringValue))))))
    (puts "")
    
    (set defaultTask ((project tasks) objectForKey:"default"))
    (if (and defaultTask ((task dependencies) count))
        (puts "default task => #{(((task dependencies) 0) name)}")
        (puts ""))
    (exit 0))

(if $printTaskDescriptions
    (puts "")
    ((((project tasks) allKeys) sort) each:
     (do (key)
         (set task ((project tasks) objectForKey:key))
         (set taskDescription (task taskDescription))
         (if (includedInOutput (task))
             (puts "nuke #{(task name)}")
             (if taskDescription
                 (puts "    #{taskDescription}"))
             (puts ""))))
    (exit 0))

(if $printDependencies
    (puts "")
    ((((project tasks) allKeys) sort) each:
     (do (key)
         (set task ((project tasks) objectForKey:key))
         (if (includedInOutput (task))
             (puts "nuke #{(task name)}")
             ((task dependencies) each:
              (do (dependency)
                  (if (and (!= "default" (dependency name)) (not (dependency isFile)))
                      (puts "     => #{(dependency name)}")))))))
    (puts "")
    (exit 0))

(if $verbose
    (((project tasks) allKeys) each:
     (do (key)
         (set mytask ((project tasks) objectForKey:key))
         (if (eq (mytask isFile) 0) (puts "#{key}")))))

(project nuke:target)
